using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Xna.Framework;
using Oops.Xna.Framework.Input;

namespace Oops.Xna.Framework.Viewpoint.Cameras
{
	// TODO : Optimize.
	// TODO : Re-evaluate.
	/// <summary>
	/// Represents a camera with chase capabilities.
	/// </summary>
	public class ChaseCamera : Camera
	{
		private ICameraCallback callback;

		/// <summary>
		/// Gets the chase object's position.
		/// </summary>
		public Vector3 ChasePosition;

		/// <summary>
		/// Gets or sets the chase object's up vector.
		/// </summary>
		private Vector3 ChaseUp = Vector3.Up;

		/// <summary>
		/// Gets or sets the relative offset for the <see cref="ChaseCamera"/> from the chase object.
		/// </summary>
		public Vector3 TargetPositionOffset = new Vector3(0.0f, 7.5f, -15.0f);
		private Vector3 targetPosition;

		/// <summary>
		/// Gets or sets the offset of the <see cref="ChaseCamera"/> focus.
		/// </summary>
		public Vector3 LookAtOffset = new Vector3(0, 2.9f, 0);
		private Vector3 lookAt;

		private float mass = 50.0f;
		private float inverseMass = 1.0f / 50.0f;

		private Vector3 chaseDirection;
		private Vector3 velocity;
		
		/// <summary>
		/// Initializes a new instances of the <see cref="ChaseCamera"/> class.
		/// </summary>
		/// <param name="game">The associated <see cref="Microsoft.Xna.Framework.Game"/>.</param>
		/// <param name="name">The name of the <see cref="ChaseCamera"/>.</param>
		/// <param name="callback">The <see cref="ICameraCallback"/> used to determine the dynamic target's position and orientation.</param>
		public ChaseCamera(Game game, string name, ICameraCallback callback) : base(game, name)
		{
			// The callback parameter must be specified.
			if (callback == null)
			{
				throw new ArgumentNullException("callback");
			}

			// Hold onto parameters.
			this.callback = callback;

			// Default some properties.
			this.Stiffness = 1800.0f;
			this.Damping = 600.0f;
		}

		/// <summary>
		/// Gets or sets the stiffness of the <see cref="ChaseCamera"/>.
		/// </summary>
		public float Stiffness { get; set; }

		/// <summary>
		/// Gets or sets the damping if the <see cref="ChaseCamera"/>.
		/// </summary>
		public float Damping { get; set; }

		/// <summary>
		/// Gets or sets the mass of the <see cref="ChaseCamera"/>.
		/// </summary>
		public float Mass
		{
			get { return this.mass; }
			set 
			{
				// Hold onto value and it's inverse.
				this.mass = value;
				this.inverseMass = 1.0f / this.mass;
			}
		}

		#region Camera Members

		protected override void UpdateView()
		{
			this.View = Matrix.CreateLookAt(this.Position, this.lookAt, this.ChaseUp);
		}

		#endregion

		#region IUpdateable Members

		public override void Update(GameTime gameTime)
		{
			// Get the elapsed time.
			float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;

			// Get the chase object's position and orientation.
			Matrix orientation = Matrix.Identity;
			this.callback.GetTransform(ref this.ChasePosition, ref orientation);
			this.chaseDirection = Vector3.Normalize(orientation.Forward);

			// Create the transform of the chase object.
			Matrix transform = Matrix.Identity;
			transform.Forward = this.chaseDirection;
			transform.Up = this.ChaseUp;
			transform.Right = Vector3.Cross(this.ChaseUp, this.chaseDirection);

			// Calculate the target position and look at in world coordinates.
			this.targetPosition = this.ChasePosition + Vector3.TransformNormal(this.TargetPositionOffset, transform);
			this.lookAt = this.ChasePosition + Vector3.TransformNormal(this.LookAtOffset, transform);

			// Calculate the spring force.
			Vector3 difference = this.Position - this.targetPosition;
			Vector3 force = difference * -this.Stiffness - this.velocity * this.Damping;

			// Apply the acceleration.
			Vector3 acceleration = force * this.inverseMass;
			this.velocity += acceleration * elapsed;

			// Apply the velocity.
			this.Position += this.velocity * elapsed;

			// TODO : Update this.Orientation.

			// Call inherited method.
			base.Update(gameTime);
		}

		#endregion
	}
}
